测试

    让我们讨论一下如何测试 Rust 代码。在这里我们不会讨论什么是测试 Rust 代码的正确方法。有很多关于写测试好坏方法的流派。所有的这些途径都使用相同的基本工具,所以我们会向你展示他们的语法。

    简单的说,测试是一个标记为test属性的函数。让我们用 Cargo 来创建一个叫adder的项目:

    在你创建一个新项目时 Cargo 会自动生成一个简单的测试。下面是src/lib.rs的内容:

    1. # // The next line exists to trick play.rust-lang.org into running our code as a
    2. # // test:
    3. # // fn main
    4. #
    5. #[cfg(test)]
    6. mod tests {
    7. #[test]
    8. fn it_works() {
    9. }
    10. }

    现在暂时去掉mod那部分,只关注函数:

    1. # // The next line exists to trick play.rust-lang.org into running our code as a
    2. # // test:
    3. # // fn main
    4. #
    5. #[test]
    6. fn it_works() {
    7. }

    注意这个#[test]。这个属性表明这是一个测试函数。它现在没有函数体。它肯定能编译通过!让我们用cargo test运行测试:

    1. $ cargo test
    2. Compiling adder v0.1.0 (file:///home/you/projects/adder)
    3. Finished debug [unoptimized + debuginfo] target(s) in 0.15 secs
    4. Running target/debug/deps/adder-941f01916ca4a642
    5. running 1 test
    6. test it_works ... ok
    7. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
    8. Doc-tests adder
    9. running 0 tests
    10. test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

    Cargo 编译和运行了我们的测试。这里有两部分输出:一个是我们写的测试,另一个是文档测试。我们稍后再讨论这些。现在,看看这行:

    1. test tests::it_works ... ok

    注意那个tests::it_works。这是我们函数的名字:

    1. # fn main() {
    2. fn it_works() {
    3. }
    4. # }

    然后我们有一个总结行:

    1. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

    那么为啥我们这个啥都没干的测试通过了呢?任何没有panic!的测试通过,panic!的测试失败。让我们的测试失败:

    1. # // The next line exists to trick play.rust-lang.org into running our code as a
    2. # // test:
    3. # // fn main
    4. #
    5. #[test]
    6. fn it_works() {
    7. assert!(false);
    8. }

    assert!是 Rust 提供的一个宏,它接受一个参数:如果参数是true,啥也不会发生。如果参数是false,它会panic!。让我们再次运行我们的测试:

    1. $ cargo test
    2. Compiling adder v0.1.0 (file:///home/you/projects/adder)
    3. Finished debug [unoptimized + debuginfo] target(s) in 0.17 secs
    4. Running target/debug/deps/adder-941f01916ca4a642
    5. running 1 test
    6. test it_works ... FAILED
    7. failures:
    8. ---- it_works stdout ----
    9. thread 'it_works' panicked at 'assertion failed: false', src/lib.rs:5
    10. note: Run with `RUST_BACKTRACE=1` for a backtrace.
    11. failures:
    12. it_works
    13. test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
    14. error: test failed

    Rust 指出我们的测试失败了:

    1. test it_works ... FAILED

    这反映在了总结行上:

    我们也得到了一个非 0 的状态码.我们在 OS X和 Linux 中使用$?

    1. $ echo $?
    2. 101
    1. > echo %ERRORLEVEL%

    而如果你使用 PowerShell:

    1. > echo $LASTEXITCODE # the code itself
    2. > echo $? # a boolean, fail or succeed

    这在你想把cargo test集成进其它工具时是非常有用。

    我们可以使用另一个属性反转我们的失败的测试:should_panic

    1. # // The next line exists to trick play.rust-lang.org into running our code as a
    2. # // test:
    3. # // fn main
    4. #
    5. #[test]
    6. #[should_panic]
    7. fn it_works() {
    8. assert!(false);
    9. }

    现在即使我们panic!了测试也会通过,并且如果我们的测试通过了则会失败。让我试一下:

    1. $ cargo test
    2. Compiling adder v0.1.0 (file:///home/you/projects/adder)
    3. Finished debug [unoptimized + debuginfo] target(s) in 0.17 secs
    4. Running target/debug/deps/adder-941f01916ca4a642
    5. running 1 test
    6. test it_works ... ok
    7. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
    8. Doc-tests adder
    9. running 0 tests
    10. test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

    Rust 提供了另一个宏,assert_eq!用来比较两个参数:

    1. # // The next line exists to trick play.rust-lang.org into running our code as a
    2. # // test:
    3. #
    4. #[test]
    5. #[should_panic]
    6. fn it_works() {
    7. }

    那个测试通过了吗?因为那个should_panic属性,它通过了:

    1. $ cargo test
    2. Compiling adder v0.1.0 (file:///home/you/projects/adder)
    3. Finished debug [unoptimized + debuginfo] target(s) in 0.21 secs
    4. Running target/debug/deps/adder-941f01916ca4a642
    5. running 1 test
    6. test it_works ... ok
    7. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
    8. Doc-tests adder
    9. running 0 tests
    10. test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

    should_panic测试是脆弱的,因为很难保证测试是否会因什么不可预测原因并未失败。为了解决这个问题,should_panic属性可以添加一个可选的expected参数。这个参数可以确保失败信息中包含我们提供的文字。下面是我们例子的一个更安全的版本:

    1. # // The next line exists to trick play.rust-lang.org into running our code as a
    2. # // test:
    3. # // fn main
    4. #
    5. #[test]
    6. #[should_panic(expected = "assertion failed")]
    7. fn it_works() {
    8. assert_eq!("Hello", "world");
    9. }

    这就是全部的基础内容!让我们写一个“真实”的测试:

    1. # // The next line exists to trick play.rust-lang.org into running our code as a
    2. # // test:
    3. # // fn main
    4. #
    5. pub fn add_two(a: i32) -> i32 {
    6. a + 2
    7. }
    8. #[test]
    9. fn it_works() {
    10. assert_eq!(4, add_two(2));
    11. }

    assert_eq!是非常常见的;用已知的参数调用一些函数然后与期望的输出进行比较。

    ignore属性

    有时一些特定的测试可能非常耗时。这时可以通过ignore属性来默认禁用:

    现在我们运行测试并发现it_works被执行了,而expensive_test没有

    1. $ cargo test
    2. Compiling adder v0.1.0 (file:///home/you/projects/adder)
    3. Finished debug [unoptimized + debuginfo] target(s) in 0.20 secs
    4. Running target/debug/deps/adder-941f01916ca4a642
    5. running 2 tests
    6. test expensive_test ... ignored
    7. test it_works ... ok
    8. test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured
    9. Doc-tests adder
    10. running 0 tests
    11. test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

    耗时的测试可以通过调用cargo test -- --ignored来执行:

    1. $ cargo test -- --ignored
    2. Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
    3. Running target/debug/deps/adder-941f01916ca4a642
    4. running 1 test
    5. test expensive_test ... ok
    6. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
    7. Doc-tests adder
    8. running 0 tests
    9. test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

    --ignored参数是 test 程序的参数,而不是 Cargo 的,这也是为什么命令是cargo test -- --ignored

    然而以这样的方式来实现我们的测试的例子并不是地道的做法:它缺少tests模块。你可能注意到了这个测试模块在最初用cargo new生成时还在代码中存在,不过在我们最后一个例子中消失了。让我们解释一下。

    一个比较惯用的做法应该是如下的:

    1. # // The next line exists to trick play.rust-lang.org into running our code as a
    2. # // test:
    3. # // fn main
    4. #
    5. pub fn add_two(a: i32) -> i32 {
    6. a + 2
    7. }
    8. #[cfg(test)]
    9. mod tests {
    10. use super::add_two;
    11. #[test]
    12. fn it_works() {
    13. assert_eq!(4, add_two(2));
    14. }
    15. }

    这里产生了一些变化。第一个变化是引入了一个cfg属性的mod tests。这个模块允许我们把所有测试集中到一起,并且需要的话还可以定义辅助函数,它们不会成为我们包装箱的一部分。cfg属性只会在我们尝试去运行测试时才会编译测试代码。这样可以节省编译时间,并且也确保我们的测试代码完全不会出现在我们的正式构建中。

    第二个变化是use声明。因为我们在一个内部模块中,我们需要把我们要测试的函数导入到当前空间中。如果你有一个大型模块的话这会非常烦人,所以这里有经常使用一个glob功能。让我们修改我们的src/lib.rs来使用这个:

    1. # // The next line exists to trick play.rust-lang.org into running our code as a
    2. # // test:
    3. # // fn main
    4. #
    5. pub fn add_two(a: i32) -> i32 {
    6. a + 2
    7. }
    8. #[cfg(test)]
    9. mod tests {
    10. use super::*;
    11. #[test]
    12. fn it_works() {
    13. assert_eq!(4, add_two(2));
    14. }
    15. }
    1. Updating registry `https://github.com/rust-lang/crates.io-index`
    2. Compiling adder v0.1.0 (file:///home/you/projects/adder)
    3. Running target/debug/deps/adder-91b3e234d4ed382a
    4. running 1 test
    5. test tests::it_works ... ok
    6. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
    7. Doc-tests adder
    8. running 0 tests
    9. test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

    它能工作了!

    目前的习惯是使用test模块来存放你的“单元测试”。任何只是测试一小部分功能的测试理应放在这里。那么“集成测试”怎么办呢?我们有tests目录来处理这些。

    tests目录

    每一个tests/*.rs文件都被当作一个独立的 crate。因此,为了进行集成测试,让我们创建一个tests目录,然后放一个tests/integration_test.rs文件进去,输入如下内容:

    1. # // The next line exists to trick play.rust-lang.org into running our code as a
    2. # // test:
    3. # // fn main
    4. #
    5. # // Sadly, this code will not work in play.rust-lang.org, because we have no
    6. # // crate adder to import. You'll need to try this part on your own machine.
    7. extern crate adder;
    8. #[test]
    9. fn it_works() {
    10. assert_eq!(4, adder::add_two(2));
    11. }

    这看起来与我们刚才的测试很像,不过有些许的不同。我们现在有一行extern crate adder在开头。这是因为在tests目录中的每个测试(文件)是一个完全不同的 crate,所以我们需要导入我们的库。这也是为什么tests是一个写集成测试的好地方:它们就像其它程序一样使用我们的库。

    让我们运行一下:

    1. $ cargo test
    2. Compiling adder v0.1.0 (file:///home/you/projects/adder)
    3. Running target/debug/deps/adder-91b3e234d4ed382a
    4. running 1 test
    5. test tests::it_works ... ok
    6. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
    7. Running target/debug/integration_test-68064b69521c828a
    8. running 1 test
    9. test it_works ... ok
    10. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
    11. Doc-tests adder
    12. running 0 tests
    13. test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

    现在我们有了三个部分:我们之前的两个测试,然后还有我们新添加的。

    Cargo (不?)会忽略tests/目录的子目录的文件。因此在集成测试中共享模块是可能的。例如tests/common/mod.rs并不会被 Cargo 单独编译并可以被任何包含mod common的测试(文件)引用。

    这就是tests目录的全部内容。它不需要test模块因为它整个就是关于测试的。

    让我们最后看看第三部分:文档测试。

    没有什么是比带有例子的文档更好的了。当然也没有什么比不能工作的例子更糟的,因为文档完成之后代码已经被改写。为此,Rust支持自动运行你文档中的例子(注意:这只在库 crate中有用,而在二进制 crate 中没用)。这是一个完整的有例子的src/lib.rs

    1. # // The next line exists to trick play.rust-lang.org into running our code as a
    2. # // test:
    3. # // fn main
    4. #
    5. //! The `adder` crate provides functions that add numbers to other numbers.
    6. //!
    7. //! # Examples
    8. //!
    9. //! ```
    10. //! assert_eq!(4, adder::add_two(2));
    11. //! ```
    12. /// This function adds two to its argument.
    13. ///
    14. /// # Examples
    15. ///
    16. /// ```
    17. /// use adder::add_two;
    18. ///
    19. /// assert_eq!(4, add_two(2));
    20. /// ```
    21. pub fn add_two(a: i32) -> i32 {
    22. a + 2
    23. }
    24. #[cfg(test)]
    25. mod tests {
    26. use super::*;
    27. #[test]
    28. fn it_works() {
    29. assert_eq!(4, add_two(2));
    30. }
    31. }

    注意模块级的文档以//!开头然后函数级的文档以///开头。Rust文档在注释中支持 Markdown 语法,所以它支持 3 个反单引号代码块语法。想上面例子那样,加入一个# Examples部分被认为是一个惯例。

    让我们再次运行测试:

    1. $ cargo test
    2. Compiling adder v0.1.0. (file:///home/you/projects/adder)
    3. Running target/debug/deps/adder-91b3e234d4ed382a
    4. running 1 test
    5. test tests::it_works ... ok
    6. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
    7. Running target/debug/integration_test-68064b69521c828a
    8. running 1 test
    9. test it_works ... ok
    10. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
    11. Doc-tests adder
    12. running 2 tests
    13. test add_two_0 ... ok
    14. test _0 ... ok
    15. test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured

    现在我们运行了3种测试!注意文档测试的名称:_0生成为模块测试,而add_two_0函数测试。如果你添加更多用例的话它们会像add_two_1这样自动加一。

    我们还没有讲到所有编写文档测试的所有细节。关于更多,请看文档章节

    测试与并发

    特别需要注意的是测试使用线程来并发的运行。为此需要注意测试之间不能相互依赖也不能依赖任何共享状态。“共享状态”可以包括运行环境,例如当前工作目录(cwd),或者环境变量。

    如果这样做有问题控制这些并发也是可能的,要么设置环境变量RUST_TEST_THREADS,或者向测试传递--test-threads用来比较两个参数:

    默认 Rust 测试标准库捕获并将输出丢弃到标准输出/错误中。例如来自println!()的输出。这也可以通过环境变量或者参数来控制:

    1. $ RUST_TEST_NOCAPTURE=1 cargo test # Preserve stdout/stderr
    2. ...